CreateApp API
何か既存のソフトウェアを自作するときにはまずそのソフトウェアはどうやって使うのかということから考えます。
実装を始めるときは低級なところから実装していく
code:playground/scr/main.ts
import { createApp, h } from 'vue'
const app = createApp({
// renderオプション
render() {
// h関数
return h('div', {}, 'Hello world.')
},
})
app.mount('#app')
まずは ↑ のように書いて、動くようにしたいが、
h() を使ったhtmlの描画は一旦置いておいて、ただのテキストだけ描画できるようにすることから始める
code: playground/src/main.ts
import { createApp } from 'vue'
const app = createApp({
render() {
return 'Hello world.'
},
})
app.mount('#app')
↑ 今回実装する開発者のインターフェース
最終的なインターフェースから考えるのめちゃ大事
ゴールから考える
code: packages/index.ts
export type Options = {
render: () => string;
};
export type App = {
mount: (selector: string) => void;
};
export const createApp = (options: Options): App => {
return {
mount: selector => {
// app.mount() の引数に渡したセレクタ(id)を探して、htmlをそのまま差しこんでるだけ
const root = document.querySelector(selector);
if (root) {
root.innerHTML = options.render();
}
}
};
};
↑ これで完成! だが、、
vueのディレクトリ構成に合わせたいのでリファクタしたい!
登場するディレクトリやファイル
runtime-core dir
vueのランタイム機能のコアな部分
コア?
code: shell
mkdir packages/runtime-core
touch packages/runtime-core/index.ts
touch packages/runtime-core/apiCreateApp.ts
touch packages/runtime-core/component.ts
touch packages/runtime-core/componentOptions.ts
touch packages/runtime-core/renderer.ts
runtime-dom
domに依存した実装を置くディレクトリ
ブラウザに依存した処理
querySelector, createElementなど
code: shell
mkdir packages/runtime-dom
touch packages/runtime-dom/index.ts
touch packages/runtime-dom/nodeOps.ts
それぞれの依存関係
https://scrapbox.io/files/65f66a7b2d36af0024ac586d.png
引用: https://ubugeeei.github.io/chibivue/10-minimum-example/010-create-app-api.html#各ファイルの役割と依存関係
リファクタしてみる
code: packages/runtime-dom/index.ts
import {
CreateAppFunction,
createAppAPI,
createRenderer,
} from '../runtime-core'
import { nodeOps } from './nodeOps'
const { render } = createRenderer(nodeOps)
const _createApp = createAppAPI(render)
export const createApp = ((...args) => {
const app = _createApp(...args)
const { mount } = app
app.mount = (selector: string) => {
const container = document.querySelector(selector)
if (!container) return
mount(container)
}
return app
}) as CreateAppFunction<Element>
code: packages/runtime-dom/nodeOps.ts
import { RendererOptions } from '../runtime-core'
// DOM に依存するオペレーション(操作)をするためのオブジェクト
export const nodeOps: RendererOptions<Node> = {
setElementText(node, text) {
node.textContent = text
},
}
code: packages/runtime-core/renderer.ts
// coreとdomで守るべきinterface
export interface RendererOptions<HostNode = RendererNode> {
setElementText(node: HostNode, text: string): void;
}
export interface RendererNode {
key: string: any;
}
export interface RendererElement extends RendererNode { }
// renderer を生成するファクトリ関数を実装
export type RootRenderFunction<HostElement = RendererElement> = (
message: string,
container: HostElement,
) => void;
export function createRenderer(options: RendererOptions) {
const { setElementText: hostSetElementText } = options;
const render: RootRenderFunction = (message, container) => {
// まだdomにテキストを直接埋め込む機能だけ
hostSetElementText(container, message);
};
return { render };
}
nodeOps (dom部分) と createRenderer (core部分) がどちらも RendererOptions というインターフェースを守ることで、両者の依存度を下げている
code: packages/runtime-core/apiCreateApp.ts
import { Component } from './component'
import { RootRenderFunction } from './renderer'
export interface App<HostElement = any> {
mount(rootContainer: HostElement | string): void
}
export type CreateAppFunction<HostElement> = (
rootComponent: Component,
) => App<HostElement>
// createApp のファクトリ関数
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent) {
const app: App = {
mount(rootContainer: HostElement) {
const message = rootComponent.render!()
render(message, rootContainer)
},
}
return app
}
}
この設計は後からの機能追加で活きてくる。今はそこまで理解しておく必要はなし。